/*
 *  Copyright (C) 1993  Robert Davis
 *
 *  This program is free software; you can redistribute it and/or
 *  modify it under the terms of Version 2, or any later version, of 
 *  the GNU General Public License as published by the Free Software 
 *  Foundation.
 */

static char RCSId[]="$Id: GnuplotPlot.m,v 1.15 1993/05/29 00:10:27 davis Exp $";


#import <appkit/Application.h>
#import <appkit/FontManager.h>
#import <appkit/Pasteboard.h>
#import <appkit/SavePanel.h>
#import <appkit/Window.h>
#import <appkit/publicWraps.h>	/* NXBeep()		*/

#import <objc/List.h>
#import <objc/NXStringTable.h>

#import <sys/stat.h>		/* stat()		*/
#import <sys/types.h>		/* stat()		*/

#import <streams/streams.h>

#import <ctype.h>		/* isspace()		*/
#import <libc.h>		/* MAXPATHLEN		*/
#import <strings.h>		/* strlen(), strcpy()	*/
#import <stdlib.h>

#import "FunctionObject.h"
#import "GnuplotPlot.h"
#import "Gnuplot.h"
#import "PlotView.h"
#import "PopUpScrollView.h"
#import "Status.h"

@interface GnuplotPlot (Private)

- _saveWithNewName:(BOOL)doNewName retainNewName:(BOOL)doRetain;
- _write:(const char *)filename;
- _read:(const char *) filename;

@end



#define DEFAULT_WIN_WIDTH	380
#define DEFAULT_WIN_HEIGHT	269


extern int scanner (char expression[]);
extern int almost_equals (int t_num, char *str);
extern char input_line[];

extern int nextfe_color;		/* Is this a color machine?	*/


/*** Preferences -- Pseudo Class Variables ***/

static int constantUpdate = UPDATE_NOT3D;	/* Replot after any change? */
						/* Default window size	*/
static NXSize winSize = { DEFAULT_WIN_WIDTH, DEFAULT_WIN_HEIGHT };



/*
 *  This function computes a new location for each new window created.  
 *  p is the origin of the windows frame, w is the size of the window.
 */
static void _newLocation (NXPoint *p, NXSize w)
{
  static count = 0;			/* This tracks all our instances */

  p->x += (21.0 * count);
  p->y -= (24.0 * count) + (w.height - DEFAULT_WIN_HEIGHT);
  count = (count > 10)? 0 : count+1;
}




static char *endOfCommand (char *aString)
{
    char *cur;
    char c;

    for (cur = aString ; cur ; cur++)
	switch (*cur) {
	case '\\':
	    if (*(cur+1) && (*(cur+1) == '\n'))
		cur++;
	    break;
	case '\n':
	case '\0':
	    return cur;
	case ';':
	    return cur;
	case '\'':
	case '"':					/* Skip quote */

	    /* Note that gnuplot does not allow nested quotes. */

	    c = *(cur++);
            while (*cur && (*cur != c) && (*cur != '\n'))
                cur++;
	    if (*cur == '\n')
		return cur;
	    break;
	case '#':
	    while (cur++ && *cur)
		if ((*cur == '\n') && (*(cur-1) != '\\'))
		    return cur;
	    break;
	}

    return NULL;
}




static char *commandcpy (char *dest, char *source)
{
    char *cursource, *curdest, *end;

    end = endOfCommand (source);

    curdest = dest;
    for (cursource = source ; cursource && (cursource != end); cursource++)
	if (*cursource == '\\' && (cursource+1 != end) 
	    && (*(cursource+1) == '\n'))
	    cursource++;
	else
	    *(curdest++) = *cursource;

    *(curdest++) = '\n';
    *curdest = '\0';

    return end;
}


/*
 *  stripCommands	Removes the 'set output "output.eps"\n', 'set 
 *			terminal ...\n', and 'pause ...' lines and 
 *			comments from the string.
 */
static char *stripCommands (char **aString)
{
    if (aString && *aString) {

	char *cur, *end, *newText;
	BOOL plotAlready = NO;

	cur = *aString;
	newText = (char *) malloc (strlen (*aString));

	*newText = '\0';

	while (cur && *cur) {
	    while (isspace(*cur))
		cur++;

	    end = commandcpy (input_line, cur);
	    scanner (input_line);

	    if (plotAlready)
		break;

	    else if (!( (almost_equals (0, "se$t")	&&

			 (almost_equals (1, "o$utput")		||
			  almost_equals (1, "t$erminal")))	||

		       almost_equals (0, "pa$use") )) {

		strcat (newText, input_line);

	    }

	    plotAlready = plotAlready || almost_equals (0, "p$lot")
				      || almost_equals (0, "sp$lot");
	    cur = (end ? end + 1 : NULL);
	}

	strcpy (*aString, newText);
	return *aString;
    }

    return NULL;
}



@implementation GnuplotPlot


+ initialize
{
    /*  
     *  This is probably temporary -- until color is handled better, 
     *  this just checks to see if we're on a color machine and sets 
     *  the color variable of the terminal accordingly.
     */
    nextfe_color = (([NXApp colorScreen]->depth) == NX_TwoBitGrayDepth)? 0 : 1;
    return self;
}



+ setConstantUpdate:(int)updateType
{
    constantUpdate = updateType;
    return self;
}


+ setHalvePlot:(BOOL)condition
{
    if (condition) {
	winSize.width = 380;
	winSize.height = 269;
    } else {
	winSize.width = 740;
	winSize.height = 521;
    }

    return [Status setHalvePlot:condition];
}


- init
{
    return [self initFromFile: NULL];
}


- initFromFile:(const char *) filename
{
    NXRect theFrame;
    const char *validSendTypes[2];

    [super init];
    controller = [NXApp delegate];
    zone = [self zone];

    [NXApp loadNibSection: "GnuplotPlot.nib"
		    owner: self
		withNames: NO
		 fromZone: zone];

    status = [[Status allocFromZone:zone] init];
    [status setDelegate: self];

    [plotScrollView setVertScrollerRequired: YES];
    [plotScrollView setHorizScrollerRequired: YES];
    [plotScrollView setBorderType:NX_BEZEL];


    plotView = [[PlotView allocFromZone:zone] initFrame:NULL];
    [[plotScrollView setDocView:plotView] free];

    stringSet = [controller stringSet];
    fullPath = NULL;

    /* Setup services */
    validSendTypes[0] = NXFilenamePboardType;
    validSendTypes[1] = NULL;
    [NXApp registerServicesMenuSendTypes: validSendTypes andReturnTypes: NULL];


    [window sizeWindow:winSize.width :winSize.height];	/* Default size	*/

    [window getFrame:&theFrame];		/* Position the window	*/
    _newLocation (&theFrame.origin, winSize);
    [window moveTo:NX_X(&theFrame) :NX_Y(&theFrame)];

    [window setMiniwindowIcon: "GnuplotDoc.tiff"];

    if (filename)  {
	[self setName:filename];
	isTitled = YES;
	[window makeKeyAndOrderFront:self];
	if (![self plotFromFile:filename]) {
	    [NXApp delayedFree:self];
	    return nil;
	}
    } else {
	[window makeKeyAndOrderFront:self];
	[self setName: NULL];
	isTitled = NO;
    }

    [self setDocEdited: NO];

    return self;
}


- free
{
    NXZoneFree (zone, fullPath);
    NXZoneFree (zone, readText);
    [status free];
    [window free];

    return [super free];
}



/*** Target/Action ***/


- save:sender
{
    return [self _saveWithNewName:NO retainNewName:YES];
}


- saveAs:sender
{
    return [self _saveWithNewName:YES retainNewName:YES];
}


- saveTo:sender
{
    return [self _saveWithNewName:YES retainNewName:NO];
}


- revertToSaved:sender
{
    switch (NXRunAlertPanel ([stringSet valueForStringKey: "revertT"],
			     [stringSet valueForStringKey: "revert"],
			     [stringSet valueForStringKey: "revertB"],
			     [stringSet valueForStringKey: "cancelB"],
			     NULL, fullPath)) {
    case NX_ALERTDEFAULT:
	[status resetCurrent];
	[self plotFromFile:fullPath];
	[self setDocEdited:NO];

	/* 
	 * Make sure any panels that are showing info about this 
	 * document know that we just changed.
	 */
	[controller updateApp];
        [[FontManager new] setSelFont:[status font] isMultiple:NO];

	return self;
	break;
    case NX_ALERTOTHER:
	return nil;
	break;
    }

    return self;
}



- close:sender
{
    [window performClose:self];
    return self;
}


- print:sender;
{
    return [plotView printPSCode:sender];
}



- setName: (const char *) name
{
    char title[255];
    char *oldFullPath = fullPath;

    if (name) {

	fullPath = NXCopyStringBufferFromZone (name, zone);
	[window setTitleAsFilename:fullPath];
	isTitled = YES;

    } else {

	sprintf (title, "%s%d", [stringSet valueForStringKey: "untitled"],
	                        [controller numberNew]);
	fullPath = NXCopyStringBufferFromZone (title, zone);
	[window setTitleAsFilename:title];
	isTitled = NO;

    }

    NXZoneFree (zone, oldFullPath);

    return self;
}


- (const char *) name
{
    return fullPath;
}


- setCurrentFont:aFont
{
    if (aFont)  {
	[status setFont:aFont];
    }

    return self;
}


- currentFont
{
    return [status font];
}



- window
{
    return window;
}


- (Status *)status
{
    return status;
}


- plotFromFile:(const char *)aFullPath
{
    id returnVal = self;

    if (![self _read:aFullPath])	/* _read into readText		*/
	return nil;

    stripCommands (&readText);		/* Ignore output, terminal, and	*/
					/* multiple plot commands	*/

    [status setAppendix: readText];

    /* 
     *  We don't check the return status of plot: because even if 
     *  there are errors, we want the file to remain open -- maybe the 
     *  user can correct the errors without having to use vi.
     */
    [self plot:self];
    [status grabCurrent];


    [status setAppendix: NULL];
    NXZoneFree (zone, readText);
    readText = NULL;

    return returnVal;
}


- plot:sender
{
    id returnVal = self;

    /* 
     *  We don't return nil if the settings don't allow us to create a 
     *  valid plot -- we simply don't plot.
     */
    if (![status canPlot]) {

	[plotView newStream:NULL];

    } else {

	char *newTitle;
	char *oldTitle = NXCopyStringBufferFromZone ([window title], zone);

	newTitle = NXZoneMalloc (zone, strlen (oldTitle) + 12);
	sprintf (newTitle, [stringSet valueForStringKey:"plotting..."],
		 oldTitle);
	[window setTitle:newTitle];
	NXPing();
	NXZoneFree (zone, oldTitle);
	NXZoneFree (zone, newTitle);

	/* 
	 *  We do, however, report an error if the settings do allow 
	 *  us to plot but we were unable to (e.g. if gnuplot bailed 
	 *  out).
	 */
	if (![status plot]) {
	    [self reportScriptError:self];
	    returnVal = nil;
	} else {

	    [plotView newStream:[status stream]];

	    /* 
	     * Make sure the scale of the doc matches the current 
	     * popUp setting
	     */
	    [plotScrollView update];	/* Not done at every window update */

	    [window update];
	}

	[window setTitleAsFilename:fullPath];
	NXPing();
    }

    return returnVal;
}




- reportScriptError: sender;
{
    NXRunAlertPanel ([stringSet valueForStringKey:"scriptErrorT"],
		     [stringSet valueForStringKey:"scriptError"],
		     NULL, NULL, NULL, [self name]);

    [plotView newStream: NULL];
    [window display];
    return self;
}


- addDataFile:(const char *)aPath
{
    FunctionObject *functionObject;

    if (![FunctionObject isAcceptableDataFile:aPath])
	return nil;

    functionObject = [[FunctionObject allocFromZone: [status zone]]
                                     initFromString: aPath
					   isThreeD: [status isThreeD]];
//    [functionObject setDataFile:YES];
    [[status functions] addObject:functionObject];
    [status reportSettingsChange:self];

    return self;
}



- setDocEdited:(BOOL) state
{
    if (((constantUpdate == UPDATE_ALWAYS) ||
	 ((constantUpdate == UPDATE_NOT3D) && ![status isThreeD])) && state)
	[self plot:self];

    [window setDocEdited:state];

    return self;
}

- (BOOL) isDocEdited
{
    return [window isDocEdited];
}



- hideDocument:sender
{
  [window orderOut:sender];
  return self;
}




/*** Services ***/


- validRequestorForSendType:(NXAtom)sendType andReturnType:(NXAtom)returnType
/*
 *  Services menu support.
 *  We are a valid requestor if the send type is filename and there is 
 *  no return data from the request.
 */
{
    return (isTitled && (sendType == NXFilenamePboardType) &&
	    (!returnType || !*returnType)) ? self : nil;
}

- (BOOL)writeSelectionToPasteboard:pboard types:(NXAtom *)types
/*
 *  Services menu support.
 *  Here we are asked by the Services menu mechanism to supply the 
 *  filename (which we said we were a valid requestor for in the above 
 *  method).
 */
{
    int save;

    if (isTitled) {
        while (types && *types)
	    if (*types == NXFilenamePboardType)
		break;
	    else 
		types++;

        if (types && *types) {
            if ([self isDocEdited]) {
		save = NXRunAlertPanel (
			[stringSet valueForStringKey:"servicesT"],
			[stringSet valueForStringKey:"serviceSave"],
			[stringSet valueForStringKey:"save"],
			[stringSet valueForStringKey:"dont save"], NULL);

                if (save == NX_ALERTDEFAULT)
		    [self save:self];
            }
            [pboard declareTypes:&NXFilenamePboardType num:1 owner:self];
            [pboard writeType:NXFilenamePboardType
	                 data:fullPath length:strlen (fullPath)+1];
            return YES;
        }
    }

    return NO;
}



/*** Autoupdate Methods ***/

- (BOOL)validateCommand:menuCell
{
    SEL action = [menuCell action];
    BOOL edited = [self isDocEdited];
    BOOL canPlot = [status canPlot];

    /* 
     *  Note that we only allow a document to be saved when the 
     *  settings validly produce a plot.  Also, if we need to see if 
     *  the plot has a file with which it is associated.
     */
    
    if (action == @selector(save:))
        return (edited || !isTitled) && canPlot;
    else if (action == @selector(saveAs:))
        return canPlot;
    else if (action == @selector(saveTo:))
        return canPlot;
    else if (action == @selector(saveAll:))	/* See Gnuplot class */
        return (edited || !isTitled) && canPlot;
    else if (action == @selector(revertToSaved:))
	return (edited && isTitled);
    else if (action == @selector(print:))
	return canPlot;
    else if (action == @selector(plot:))
	return canPlot;

    return YES;
}
	



/*** Window Delegate Methods ***/

- windowDidBecomeMain:sender
{
    /* Update font panel */
    if (plotView)  {
	[window makeFirstResponder: plotView];
//	[[FontManager new] setSelFont:[status font] isMultiple:NO];
    }

    [controller setCurrentDoc:self];

    return self;
}



- windowDidMiniaturize:sender
{
    [controller setCurrentDoc:nil];
    return self;
}



- windowDidResignMain:sender
{
//    [[FontManager new] setEnabled:NO];
    return self;
}



- windowDidResize:sender
{
    int aTag;

    /*
     *  Make sure the scale of the doc matches the current popUp 
     *  setting.  (If it's set to "Always Fit," which is 0, then resizing 
     *  window should cause it to rescale.)
     */
    aTag = [plotScrollView currentTag];
    if (!aTag)
	[plotScrollView setCurrentTag: aTag];

    return self;
}


- windowDidUpdate:sender
{
    [[FontManager new] setEnabled:([status canPlot] && [window isMainWindow])];
    return self;
}



- windowWillClose:sender
{
    id returnVal = self;
    BOOL done = NO;

    if ([self isDocEdited] && [status canPlot])  {

	while (!done)  {	/* Keep asking until user tells what to do */

	    switch (NXRunAlertPanel([stringSet valueForStringKey:"willCloseT"],
				    [stringSet valueForStringKey:"willClose"],
				    [stringSet valueForStringKey:"save"],
				    [stringSet valueForStringKey:"dont save"],
				    [stringSet valueForStringKey:"cancelB"],
				    [self name]))  {
	    case NX_ALERTDEFAULT:		/* Save		*/
		if (returnVal = [self save:self])
		    done = YES;
		break;
	    case NX_ALERTALTERNATE:
		returnVal = self;		/* Don't Save	*/
		done = YES;
		break;
	    case NX_ALERTOTHER:			/* Cancel	*/
		returnVal = nil;
		done = YES;
		break;

	    }

	}

    }

    if (returnVal)  {
	plotScrollView = nil;		/* Don't want stale instance vars */
	plotView = nil;

	[window orderOut:self];
	[controller docDidClose:self];
	[NXApp delayedFree: self];
    }

    return returnVal;
}


- windowWillResize:sender toSize:(NXSize *) frameSize
{
    NXRect rect;
    float x, y, w, h;

    [plotView getFrame:&rect];

    /*  
     *  If the frame rectangle of the view has both a length and a 
     *  width, i.e. if there's something to see, we control the sizing 
     *  so it can never be bigger than the rectangle (except to allow 
     *  room for border and sliders).
     */

    if ((w = NX_WIDTH(&rect)) && (h = NX_HEIGHT(&rect)))  {

	x = w + 22;			/* Make room for borders & sliders */
	y = h + 49;

			/* If the current setting isn't "Always Fit"	*/
	if ([plotScrollView currentTag])  {

	    if (frameSize->width > x) frameSize->width = x;
	    if (frameSize->height > y) frameSize->height = y;

	}

    }

/*
 *  Don't allow the window to get small enough to lose the scroll 
 *  buttons and the pop-up list.
 */

    if (frameSize->width < 147) frameSize->width = 147;
    if (frameSize->height < 65) frameSize->height = 65;

    return self;
}



/*** Status Delegate Methods ***/


- settingsDidChange:sender
{
    [self setDocEdited: YES];
    return self;
}



// Shuts up the compiler about unused RCSId
- (const char *) rcsid
{
    return RCSId;
}


@end



@implementation GnuplotPlot (Private)


/*
 *  This method is based on a similar method from the NeXTSTEP 
 *  Developer Example WhatsUpDoc.  All varieties of save go through 
 *  this routine.  It covers all the cases of running the Save Panel 
 *  and retaining the name chosen.
 */
- _saveWithNewName:(BOOL)doNewName retainNewName:(BOOL)doRetain
{
    const char	*saveName;		/* filename to save into */
    id		savePanel = [SavePanel new];

    if (doNewName || !isTitled) {	/* saveAs or saveTo */

	if ([savePanel runModalForDirectory:fullPath
	           file: (isTitled ? rindex (fullPath, '/') + 1 
						  : [self name])]) {
	    saveName = [savePanel filename];
	} else
	    return nil;			/* cancelled from SavePanel */


    } else {				/* ordinary Save */

	/* 
	 *  In order to save a document, it must be able to produce a 
	 *  valid plot and it must either be edited or be untitled.
	 */
	if ((![self isDocEdited] && isTitled) || ![status canPlot])
	    return nil;

	saveName = fullPath;
    }

    switch ([controller saveType]) {
    case SAVE_GNUPLOT:

	if (![self _write:saveName])  {
	    NXRunAlertPanel ([stringSet valueForStringKey: "Save"],
			     [stringSet valueForStringKey: "cantSave"],
			     [stringSet valueForStringKey: "OKB"],
			     NULL, NULL, saveName);
	    return nil;
	}
					/* Update the document name	*/
	if (doRetain) {			/* if requested			*/
	    [self setName:saveName];
	    [self setDocEdited:NO];
	}
	break;

    case SAVE_EPS:

	if (![plotView saveEPSToFile:[savePanel filename]]) {
	    NXRunAlertPanel ([stringSet valueForStringKey: "Save"],
			     [stringSet valueForStringKey: "cantSave"],
			     [stringSet valueForStringKey: "OKB"],
			     NULL, NULL, saveName);
	    return nil;
	}

	break;

    case SAVE_TIFF:

	if (![plotView saveTIFFToFile:[savePanel filename]]) {
	    NXRunAlertPanel ([stringSet valueForStringKey: "Save"],
			     [stringSet valueForStringKey: "cantSave"],
			     [stringSet valueForStringKey: "OKB"],
			     NULL, NULL, saveName);
	    return nil;
	}

	break;
    }

    return self;
}



/*
 *  This method does all the writing so any application-specific stuff 
 *  should go here.
 */
- _write:(const char *)filename
{
    NXModalSession *session;
    id modalPanel = NXGetAlertPanel ("Save", "Replotting %s.",
				     NULL,NULL,NULL, [self name]);

    if ([Status lastplot] != status)  {
	session = [NXApp beginModalSession:NULL for:modalPanel];
	[self plot:self];
	[NXApp endModalSession:session];
	NXFreeAlertPanel(modalPanel);
    }

    return [status saveToFile:filename];
}




- _read:(const char *) filename
{
    NXStream *stream;
    struct stat fileinfo;

    if (stat (filename, &fileinfo)) {
	NXRunAlertPanel ([stringSet valueForStringKey: "openT"],
			 [stringSet valueForStringKey: "cantOpen"],
			 [stringSet valueForStringKey: "OKB"],
			 NULL, NULL, filename);
	return nil;
    }

    if (stream = NXMapFile (filename, NX_READONLY)) {
	if (readText)
	    NXZoneFree (zone, readText);
	readText = NXZoneMalloc (zone, fileinfo.st_size + 1);
	NXRead (stream, readText, fileinfo.st_size);
	readText[fileinfo.st_size] = '\0';
	NXClose (stream);

	/* Check to see if the file we read is writable -- foreshadowing */
        if (!(fileinfo.st_mode & S_IWRITE))
	    NXRunAlertPanel ([stringSet valueForStringKey: "warningT"],
			     [stringSet valueForStringKey: "unwritable"],
			     [stringSet valueForStringKey: "OKB"],
			     NULL, NULL, filename);
    } else {
	NXRunAlertPanel ([stringSet valueForStringKey: "openT"],
			 [stringSet valueForStringKey: "cantOpen"],
			 [stringSet valueForStringKey: "OKB"],
			 NULL, NULL, filename);
	return nil;
    }

    return self;
}


@end
